﻿using System;
using System.Text;

namespace SkeinLib
{   
    public class Skein256Ex
    {
        // Between the 3 state sizes of Skein (specifically the Skein[StateSize]Ex classes), these are the only variables that change.
        // This is by design, as the classes are essentially identical save for some very specific values (initializers, some documentation,
        // and these variables).  This makes modifications easier as almost the entire class can be copy-pasted to maintain consistancy.
        // Note:  Abstraction to a class from which the 3 are derived won't work the way they are currently written.  An experiment into
        //  this determined that the state-width specific aspects of the code don't properly cascade through the base-class, and thus
        //  the process fails.  So... copy pasta!
        //========================================================start state dependant variables
        const int DEFAULTHASHBITLENGTH = 256;
        const int WORDWIDTH = 4;
        //========================================================end state dependant variables
        // NOTE TO CODER: make sure the documentation XML in the classes get updated if the functions are copied-and-pasted 
        internal UInt64[] _state = null;
        internal int? _hashBitLength = null;  // this is nullable to help determine if the class has been properly initialized
        internal byte _treeMaxLevels = 0;
        internal byte _treeLeaf = 0;
        internal byte _treeNode = 0;
        // in order of importance
        // personalizations cannot be added after a public key, and a public key cannot be added after a nonce
        // all three can be used, but it must be in the order listed.
        internal bool _hasPersonalization = false;
        internal bool _hasAPublicKey = false;
        internal bool _hasNonce = false;

        /// <summary>
        /// Gets a boolean indicating if a personalization has been applied to this instance.
        /// </summary>
        public bool IsPersonalized { get { return _hasPersonalization; } }
        /// <summary>
        /// Gets a boolean indicating if a public key has been applied to this instance.
        /// </summary>
        public bool HasAPublicKey { get { return _hasAPublicKey; } }
        /// <summary>
        /// Gets a boolean indicating if a NONCE has been applied to this instance.
        /// </summary>
        public bool HasANonce { get { return _hasNonce; } }

        #region "Public Class Constructors"

        /// <summary>
        /// Default initializaion constuctor.  
        /// Object is not ready to hash if the default construtor is used.  
        /// Use Initialize() to prepare for hashing.
        /// </summary>
        public Skein256Ex() { }

        /// <summary>
        /// Initialize with a given hashBitLength.
        /// </summary>
        /// <param name="hashBitLength">[OPTIONAL] The length in bits of the final output. Default=256, must be >= 8 and multiple of 8</param>
        /// <param name="key">[OPTIONAL] If the hash operation is to be keyed (MAC), the key is passed here as an array of bytes. Default=null</param>
        /// <exception cref=""
        public Skein256Ex(int hashBitLength = DEFAULTHASHBITLENGTH, byte[] key = null)
        {
            this.Initialize(hashBitLength, key);
        }

        ~Skein256Ex()
        {
            this.Clear();
        }

        #endregion

        #region "Initializers"

        /// <summary>
        /// Default initializaion constuctor.  Default=256 hashbitlength, no tree-hashing, no personalization.
        /// </summary>
        public void Initialize()
        {
            _state = SkeinCore.GetInitialUnkeyedState(WORDWIDTH);
            _hashBitLength = DEFAULTHASHBITLENGTH;
            _treeLeaf = 0;
            _treeNode = 0;
            _treeMaxLevels = 0;
        }

        /// <summary>
        /// Initialize with a key for MAC operations, and a given hashBitLentgh output.
        /// NOTE:  Tree hashing process produces unique values when compared to straight-hashing.  See the Skein white paper for more details.
        /// When using Tree Hashing, all three related variables must be greater than 0, and the Tree Max Level must be greater than 1.
        /// </summary>
        /// <param name="hashBitLength">[OPTIONAL] The length in bits of the final output. Default=256, must be >=8 and multiple of 8</param>
        /// <param name="key">[OPTIONAL] The key, as a byte array, against which to hash (MAC operations). Default=null</param>
        /// <param name="personalization">[OPTIONAL]  This string will be processed into the hash to make the output unique to a particular usage. Default=null</param>
        /// <param name="treeLeaf">[OPTIONAL] Tree Hashing: specifies the width of the leaves in the first pass of a tree-hash. Default=0</param>
        /// <param name="treeNode">[OPTIONAL] Tree Hashing: specifies the width of the nodes in level 2 and higher of the tree-hash. Default=0</param>
        /// <param name="treeMaxLevels">[OPTIONAL] Tree Hashing: specifies the max level to process data, must be >= 2. Default=0</param>
        public void Initialize(int hashBitLength = DEFAULTHASHBITLENGTH, 
                               byte[] key = null,
                               string personalization = null,
                               byte treeLeaf = 0,
                               byte treeNode = 0,
                               byte treeMaxLevels = 0)
        {   
            if (hashBitLength < 1)
                throw new ArgumentOutOfRangeException("hashBitLenth", "Initialize:  hashBitLength cannot be Zero or negative.");
            _state = SkeinCore.Initialize(WORDWIDTH, (ulong)hashBitLength, key, treeLeaf, treeNode, treeMaxLevels);
            if (!string.IsNullOrWhiteSpace(personalization))
                SkeinCore.ProcessPersonalization(ref _state, ref personalization);
            _hashBitLength = hashBitLength;
            _treeLeaf = (byte)treeLeaf;
            _treeNode = (byte)treeNode;
            _treeMaxLevels = (byte)treeMaxLevels;
        }

        #endregion

        /// <summary>
        /// Produce a hash against an array of bytes.
        /// After the result is calculated, the state is wiped (one-time use computation).
        /// </summary>
        /// <param name="data">The data, as a byte array, to be hashed.</param>
        /// <param name="lastByteBitCount">[OPTIONAL] If the last byte contains fewer than 8 actual data bits (packed in the high end of the byte),
        /// indicate how many bits to process. Default=0, must be less than 8.</param>
        /// <returns>The byte array containing the resulting hash.</returns>
        public byte[] ComputeHash(byte[] data, byte lastByteBitCount = 0)
        {
            if (_state == null || _hashBitLength == null)
                throw new InvalidOperationException("Not initialized.  Call appropriate Initialize method first.");
            if (data == null)
               data = new byte[0];
            if (_treeLeaf == 0 && _treeNode == 0 && _treeMaxLevels == 0)
                SkeinCore.ProcessMessage(ref _state, data, lastByteBitCount);
            else
                SkeinCore.ProcessMessage(ref _state, data, lastByteBitCount, _treeLeaf, _treeNode, _treeMaxLevels);
            byte[] result = SkeinCore.ProcessStandardOutput(ref _state, (ulong)_hashBitLength);
            _hashBitLength = null;
            Clear();
            return result;
        }

        /// <summary>
        /// Produce a hash against a string.
        /// After the result is calculated, the state is wiped (one-time use computation).
        /// </summary>
        /// <param name="data">The string to be hashed.</param>
        /// <returns>The byte array containing the resulting hash.</returns>
        public byte[] ComputeHash(ref string data)
        {
            if (_state == null || _hashBitLength == null)
                throw new InvalidOperationException("Not initialized.  Call appropriate Initialize method first.");
            SkeinCore.ProcessMessage(ref _state, ref data);
            byte[] result = SkeinCore.ProcessStandardOutput(ref _state, (ulong)_hashBitLength);
            _hashBitLength = null;
            Clear();
            return result;
        }

        /// <summary>
        /// Wipe out the state to prevent leaking information.
        /// The appropriate Initialize() method must be called afterwards in order to produce more hashes.
        /// NOTE:  This is done automatically after a ComputeHash operation.
        /// </summary>
        public void Clear()
        {
            SkeinCore.ClearWords(ref _state, true);
            _hashBitLength = null;
            _treeLeaf = 0;
            _treeNode = 0;
            _treeMaxLevels = 0;
            _hasPersonalization = false;
            _hasAPublicKey = false;
            _hasNonce = false;
        }

        #region Advanced Methods

        #region -Personalization
        /// <summary>
        /// Apply a personalization to this instance of the hash function to make it unique to a given application or process.
        /// This must be done before applying a public key and/or nonce.
        /// </summary>
        /// <param name="personalization">String containing the personalization</param>
        public void Personalize(string personalization)
        {
            if (_state == null || _hashBitLength == null)
                throw new InvalidOperationException("Not initialized.  Call appropriate Initialize method first.");
            if (IsPersonalized)
                throw new InvalidOperationException("An instance cannot be personalized more than once.");
            if (HasAPublicKey || HasANonce)
                throw new InvalidOperationException("Personalization can only be done before processing a public key and/or a nonce.");
            _hasPersonalization = true;
            SkeinCore.ProcessPersonalization(ref _state, ref personalization);
        }

        /// <summary>
        /// Apply a personalization to this instance of the hash function to make it unique to a given application or process.
        /// This must be done before applying a public key and/or nonce.
        /// </summary>
        /// <param name="personalization">The personalization as an array of bytes.</param>
        public void Personalize(byte[] personalization)
        {
            if (_state == null || _hashBitLength == null)
                throw new InvalidOperationException("Not initialized.  Call appropriate Initialize method first.");
            if (IsPersonalized)
                throw new InvalidOperationException("An instance cannot be personalized more than once.");
            if (HasAPublicKey || HasANonce)
                throw new InvalidOperationException("Personalization can only be done before processing a public key and/or a nonce.");
            _hasPersonalization = true;
            SkeinCore.ProcessPersonalization(ref _state, personalization);
        }

        /// <summary>
        /// Use this to tie the hash process to a particular public key.  Especially useful and secure when creating signed message digests.
        /// NOTE:  This instance does not store any information about the keys or their order!  They are folded in the moment the method returns.
        /// </summary>
        /// <param name="publicKeyMaterial">The public key to attach to the hashing instance as an array of bytes.</param>
        public void AddPublicKey(byte[] publicKeyMaterial)
        {
            if (_state == null || _hashBitLength == null)
                throw new InvalidOperationException("Not initialized.  Call appropriate Initialize method first.");
            // not sure about this one:
            //if (HasAPublicKey)
            //    throw new InvalidOperationException("Only one public key can be attached to a given hashing instance.");
            if (HasANonce)
                throw new InvalidOperationException("A public key cannot be added after processing a nonce.");
            _hasAPublicKey = true;
            SkeinCore.ProcessPublicKey(ref _state, publicKeyMaterial);
        }

        /// <summary>
        /// Use this to tie a NONCE to the hash process.  Normally used for randomization or random hashing.  
        /// Most NONCE's cannot/should not be used for more than one hash process for security reasons.
        /// </summary>
        /// <param name="publicKeyMaterial">The public key to attach to the hashing instance.</param>
        public void AddNonce(byte[] nonce)
        {
            if (_state == null || _hashBitLength == null)
                throw new InvalidOperationException("Not initialized.  Call appropriate Initialize method first.");
            if (HasANonce)
                throw new InvalidOperationException("Only one NONCE can be attached to a given hashing instance.");
            _hasNonce = true;
            SkeinCore.ProcessNonce(ref _state, nonce);
        }

        #endregion -Personalization

        #region -Key Derivation
        /// <summary>
        /// Static Method:  Derive a key from a master and an identifier.  Good for creating IV's or derivative keys.
        /// Calling this method does not affect any initialized instances.
        /// </summary>
        /// <param name="masterKey">The master key from which a new key will be derived.</param>
        /// <param name="identifier">The unique identifier for the new key, which is processed as part of the hash.</param>
        /// <param name="hashBitLength">The length, in bits (byte aligned) for the final output.  Default=256</param>
        /// <param name="salt">Any additional salt material, as an array of bytes, to hash along with the master key and identifier (recommended).</param>
        /// <returns>The derived key of the specified bit length as an array of bytes.</returns>
        public static byte[] DeriveKey(byte[] masterKey, string identifier, int hashBitLength = DEFAULTHASHBITLENGTH, byte[] salt = null)
        {
            return SkeinCore.DeriveKey(WORDWIDTH, masterKey, ref identifier, hashBitLength, salt); 
        }

        /// <summary>
        /// Static Method:  Derive a key from a master and an identifier.  Good for creating IV's or derivitive keys.
        /// Calling this method does not affect any initialized instances.
        /// </summary>
        /// <param name="masterKey">The master key from which a new key will be derived.</param>
        /// <param name="identifier">The unique identifier for the new key, which is processed as part of the hash, as an array of bytes.</param>
        /// <param name="hashBitLength">The length, in bits (byte aligned) for the final output.  Default=256</param>
        /// <param name="salt">Any additional salt material, as an array of bytes, to hash along with the master key and identifier (recommended).</param>
        /// <returns>The derived key of the specified bit length as an array of bytes.</returns>
        public static byte[] DeriveKey(byte[] masterKey, byte[] identifier, int hashBitLength = DEFAULTHASHBITLENGTH, byte[] salt = null)
        {
            return SkeinCore.DeriveKey(WORDWIDTH, masterKey, identifier, hashBitLength, salt);
        }

        /// <summary>
        /// Provides for a PKCS #5 (RFC2898:PBKDF1) 'style' password derivation function using Skein.
        /// </summary>
        /// <param name="password">The password string from which a key will be derived. (UTF-8 processed internally)</param>
        /// <param name="salt">The salt to add to the password during transformation.  NOT OPTIONAL, Range 8 to 32 bytes</param>
        /// <param name="iterationCount">[OPTIONAL] The number of times to expand out the password and the salt.  Default = 80, Range 10 to 1000</param>
        /// <param name="hashBitLength">[OPTIONAL] The length in bits of the final key.  Default=256, cannot be 0</param>
        /// <returns>The derived key of the specified bit length as an array of bytes.</returns>
        public static byte[] DeriveKeyFromPassword(string password, byte[] salt, short iterationCount = 80, int hashBitLength = DEFAULTHASHBITLENGTH)
        {
            return SkeinCore.DeriveKeyFromPassword(WORDWIDTH, ref password, salt, iterationCount, hashBitLength);
        }

        /// <summary>
        /// Provides for a PKCS #5 (RFC2898:PBKDF1) 'style' password derivation function using Skein.
        /// </summary>
        /// <param name="password">The password as an array of bytes from which a key will be derived.</param>
        /// <param name="salt">The salt to add to the password during transformation.  NOT OPTIONAL, Range 8 to 32 bytes</param>
        /// <param name="iterationCount">[OPTIONAL] The number of times to expand out the password and the salt.  Default = 80, Range 10 to 1000</param>
        /// <param name="hashBitLength">[OPTIONAL] The length in bits of the final key.  Default=256, cannot be 0</param>
        /// <returns>The derived key of the specified bit length as an array of bytes.</returns>
        public static byte[] DeriveKeyFromPassword(byte[] password, byte[] salt, short iterationCount = 80, int hashBitLength = DEFAULTHASHBITLENGTH)
        {
            return SkeinCore.DeriveKeyFromPassword(WORDWIDTH, password, salt, iterationCount, hashBitLength);
        }
        #endregion -Key Derivation
        #endregion Advanced Methods

    }//end class
}
